﻿#region License
/**	
The MIT License (MIT)

Copyright (c) 2013 Philipp Schillinger, Michael Alekseew, Andreas Braun, Steeven Zeiß

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. 
 **/
#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Net.Sockets;
using System.Net;

namespace IndoorLocalization {

	/// <summary>
	/// Provides an interface for easy communication between multiple PCs running this program.
	/// </summary>
	/*static*/ class NetworkConnector {

		/// <summary>
		/// The endpoint used for multicasts.
		/// </summary>
		private static IPEndPoint MULTICAST;

		/// <summary>
		/// The thread that sends own entities to all other program instances via the network.
		/// </summary>
		private static Thread networkAgentSender;
		/// <summary>
		/// Indicates whether the network is alive or not.
		/// </summary>
		private static bool isAlive;

		/// <summary>
		/// The UPD-Client used for sending udp messages.
		/// </summary>
		private static UdpClient udp;
		/// <summary>
		/// The UDP-Client for asynchronous receives.
		/// </summary>
		private static UdpClient recv;

		private static byte networkID;
		/// <summary>
		/// A unique byte in the network to determine who sent an entity.
		/// </summary>
		public static byte NetworkID { get { return networkID; } }

		/// <summary>
		/// Resolves network IDs to their corresponding IP adresses including the own.
		/// </summary>
		private static Dictionary<byte, IPAddress> idResolution = new Dictionary<byte,IPAddress>();

		/// <summary>
		/// Indicates how many other applications are currently known (connected).
		/// </summary>
		public static int EndPoints { get { return idResolution.Count - 1; } }

		/// <summary>
		/// A list of all entities that are currently reported by the network.
		/// </summary>
		private static List<Entity> entities = new List<Entity>();

		/// <summary>
		/// Indicates how much entities of other applications are currently active.
		/// </summary>
		public static int NumberOfEntities { get { return entities.Count; } }

		/// <summary>
		/// [Threadsafe] Returns an (independent) list of all entities
		/// currently reported by the network.
		/// </summary>
		/// <returns>A list of currently found entities.</returns>
		public static List<Entity> GetEntitiesTS() {
			List<Entity> result = new List<Entity>();
			lock (entities) {
				foreach (Entity entity in entities)
					result.Add(entity);
			}
			return result;
		}

		/// <summary>
		/// Starts participating in the network by initializing UDP and subscribing
		/// to the multicast-address.
		/// </summary>
		public static void StartNetwork() {
			// create UDP socket
			try {
				MULTICAST = new IPEndPoint(IPAddress.Parse("230.0.0.1"), Program.NetworkPort);
				udp = new UdpClient();
				udp.EnableBroadcast = true;
				udp.JoinMulticastGroup(MULTICAST.Address);
				recv = new UdpClient(Program.NetworkPort);
				recv.EnableBroadcast = true;
				recv.JoinMulticastGroup(MULTICAST.Address);
			} catch (SocketException) {
				Program.Error("Failed to start network, please try again");
				return;
			}

			// initialize the ID
			// ID is set by the last segment of the own local IP address
			// considering all clients are in the same class c subnet
			// (which means the last segment is unique)
			IPHostEntry host = Dns.GetHostEntry(Dns.GetHostName());
			foreach (IPAddress ip in host.AddressList) {
				if (ip.AddressFamily == AddressFamily.InterNetwork
					&& !ip.ToString().Equals("192.168.56.1")) { // ignore VirtualBox Network
					networkID = ip.GetAddressBytes().Last();
					idResolution.Add(networkID, ip);
					break;
				}
			}
			Program.Log("Started network, ID: " + networkID);

			isAlive = true;

			// send welcome message
			byte[] welcomeMessage = Message.CreateWelcomeMessage(networkID, idResolution[networkID], Program.Mergers);
			udp.Send(welcomeMessage, welcomeMessage.Length, MULTICAST);

			// start network threads
			networkAgentSender = new Thread(SyncEntities);
			networkAgentSender.IsBackground = true;
			networkAgentSender.Start();

			recv.BeginReceive(new AsyncCallback(Receive), null);
		}

		/// <summary>
		/// Closes the connection to the network.
		/// </summary>
		public static void StopNetwork() {
			Program.Log("Stopping network");

			// stop network thread
			isAlive = false;
			networkAgentSender.Join();
			networkAgentSender.Abort();

			// send goodbye message
			byte[] goodbyeMessage = Message.CreateGoodbyeMessage(networkID);
			udp.Send(goodbyeMessage, goodbyeMessage.Length, MULTICAST);

			// search for cams that use remote mergers
			// set their merger to the local default merger and pause them
			bool foundAny = false;
			foreach (SensorCam cam in Program.Cameras) {
				if (!cam.Merger.IsLocal()) {
					foundAny = true;
					cam.Merger = Program.Merger;
					cam.Pause();
				}
			}
			if (foundAny)
				Program.Warn("Some cams used remote mergers, they have been paused");

			// remove entities of the network
			lock (entities) {
				entities.Clear();
			}

			// remove all network rays
			foreach (SensorMerger merger in Program.Mergers) {
				merger.ClearRemoteRaysTS();
			}

			// end UDP communication
			recv.DropMulticastGroup(MULTICAST.Address);
			recv.Close();
			udp.DropMulticastGroup(MULTICAST.Address);
			udp.Close();

			// clear ID resolution list and remote mergers
			idResolution.Clear();
			Program.RemoteMergers.Clear();

			// remove all network rays
			foreach (SensorMerger merger in Program.Mergers) {
				merger.ClearRemoteRaysTS();
			}
		}

		/// <summary>
		/// Endless loop to execute the synchronization of entities.
		/// Should be started as a background thread.
		/// </summary>
		private static void SyncEntities() {
			while (isAlive) {
				Thread.Sleep(500); // wait to reduce network load

				// collect entities to send
				List<Entity> toSend = new List<Entity>();
				foreach (SensorMerger merger in Program.Mergers)
					toSend.AddRange(merger.GetEntitiesTS());

				// send own entities
				byte[] sendData = Message.CreateEntitiesMessage(networkID, toSend);
				try {
					udp.Send(sendData, sendData.Length, MULTICAST);
				} catch (SocketException) {
					Program.Error("Failed to synchronize entities, network is probably broken");
				}
			}
		}

		/// <summary>
		/// Sends a MergerChanged message to the network to indicate that a merger has been
		/// added or removed.
		/// </summary>
		/// <param name="mergerName">The name of the merger that changed.</param>
		public static void SendMergerChanged(String mergerName) {
			if (!isAlive) return;
			byte[] mergerChangedMessage = Message.CreateMergerChangedMessage(networkID, mergerName);
			udp.Send(mergerChangedMessage, mergerChangedMessage.Length, MULTICAST);
		}

		/// <summary>
		/// Sends a P2P message with all found rays to the specified RemoteMerger.
		/// </summary>
		/// <param name="remoteMerger">The RemoteMerger used to merge the rays.</param>
		/// <param name="camGuid">The cam which found the rays to update them remotely.</param>
		/// <param name="rays">A list of all found rays.</param>
		public static void SendRaysToRemoteMerger(RemoteMerger remoteMerger, Guid camGuid, List<SensorRay> rays) {
			if (!isAlive) return;
			byte[] message = Message.CreateRaysMessage(networkID, remoteMerger.Name, camGuid, rays);

			// find IP of the target application that is running the remote merger
			if (!idResolution.ContainsKey(remoteMerger.NetworkID)) {
				Program.Error("Network ID " + remoteMerger.NetworkID + " not found!");
				SensorCam cam = Program.Cameras.Find(new Predicate<SensorCam>(target => {
					return target.GUID.Equals(camGuid);
				}));
				cam.Pause();
				cam.Merger = Program.Merger;
				Program.Error("Camera "+cam.Name+" has been paused because neccessary network ID was not found");
				return;
			}
			IPEndPoint endPoint = new IPEndPoint(idResolution[remoteMerger.NetworkID], Program.NetworkPort);
			udp.Send(message, message.Length, endPoint);
		}
		
		/// <summary>
		/// Callback to receive data
		/// </summary>
		private static void Receive(IAsyncResult ar) {
			// receive multicasts
			IPEndPoint ep = new IPEndPoint(IPAddress.Any, 0);
			try {
				byte[] received = recv.EndReceive(ar, ref ep);
				byte sender = Message.GetSenderID(received);

				// be ready for other messages
				if (isAlive) recv.BeginReceive(Receive, null);

				// skip own datagrams
				if (sender != networkID) {

					switch (Message.GetType(received)) {
						#region Welcome
						// ---[ Welcome message ]-------------------------
						case Message.Type.Welcome:
						case Message.Type.WelcomeCallback:
							// decode message
							IPAddress senderIP = null;
							List<SensorMerger> mergers = new List<SensorMerger>();
							Message.DecodeWelcomeMessage(received, ref senderIP, ref mergers);

							if(Message.GetType(received) == Message.Type.Welcome)
								Program.Log("Network id " + sender + " connected");
							else
								Program.Log("Network id " + sender + " is already connected");


							// create entry in resolution list
							idResolution.Add(sender, senderIP);

							// add remote mergers
							foreach (SensorMerger merger in mergers)
								Program.RemoteMergers.Add(new RemoteMerger(merger.Name, sender));

							// respond with a P2P welcome message if not a callback
							if (Message.GetType(received) == Message.Type.WelcomeCallback) break;

							byte[] message = Message.CreateWelcomeCallbackMessage(networkID, idResolution[networkID], Program.Mergers);
							IPEndPoint endPoint = new IPEndPoint(senderIP, Program.NetworkPort);
							udp.Send(message, message.Length, endPoint);

							break;
						#endregion

						#region Goodbye
						// ---[ Goodbye message ]-------------------------
						case Message.Type.Goodbye:
							// delete entry in resolution list
							idResolution.Remove(sender);

							Program.Log("Network id "+sender+" disconnected");

							// remove disconnected remote mergers
							RemoveRemoteMerger(sender, null);

							// remove entities of this id
							lock (entities) {
								entities.RemoveAll(new Predicate<Entity>(target => {
									return target.NetworkID == sender;
								}));
							}

							// remove all network rays
							// (it is not possible to find out which network rays have been disconnected
							// but those that don't will be sent again)
							foreach (SensorMerger merger in Program.Mergers) {
								merger.ClearRemoteRaysTS();
							}

							break;
						#endregion

						#region Entities
						// ---[ Entities message ]------------------------
						case Message.Type.Entities:
							// decode message
							List<Entity> receivedEntities = new List<Entity>();
							Message.DecodeEntitiesMessage(received, ref receivedEntities);

							// update entity list
							lock (entities) {
								// remove old entities of this id
								entities.RemoveAll(new Predicate<Entity>(target => {
									return target.NetworkID == sender;
								}));
								// add the new ones
								entities.AddRange(receivedEntities);
							}

							break;
						#endregion

						#region Rays
						// ---[ Rays message ]----------------------------
						case Message.Type.Rays:
							// decode message
							String localMergerName = "";
							Guid camGuid = new Guid();
							List<SensorRay> rays = new List<SensorRay>();
							Message.DecodeRaysMessage(received, ref localMergerName, ref camGuid, ref rays);

							// find the local merger
							SensorMerger localMerger =
								Program.Mergers.Find(new Predicate<SensorMerger>(target => {
									return target.Name.Equals(localMergerName);
								}));

							// update rays
							localMerger.UpdateRaysTS(camGuid, rays);

							break;
						#endregion

						#region MergerChanged
						// ---[ MergerChanged message ]-------------------
						case Message.Type.MergerChanged:
							// decode message
							String mergerName = "";
							Message.DecodeMergerChangedMessage(received, ref mergerName);

							// try to find remote merger
							RemoteMerger remoteMerger =
								Program.RemoteMergers.Find(new Predicate<RemoteMerger>(target => {
									return target.NetworkID == sender && target.Name.Equals(mergerName);
								}));

							// add or remove the specified remote merger
							if (remoteMerger == null) {
								Program.RemoteMergers.Add(new RemoteMerger(mergerName, sender));
								Program.Log("The merger "+mergerName+" was added by id "+sender);
							} else {
								Program.RemoteMergers.Remove(remoteMerger);
								Program.Log("The merger " + mergerName + " at id " + sender + " was removed");
								RemoveRemoteMerger(sender, mergerName);
							}

							break;
						#endregion

						default:
							Program.Warn("Unknown message type received");
							break;
					}


				}
			} catch (ObjectDisposedException) {
				// This exception has to be catched due to microsofts specifications:
				// If the connection is terminated as it should (.Close()), each asynchronous
				// receive will throw this exception.
			}
		}

		/// <summary>
		/// Removes a disconnected remote merger from the list of remote mergers.
		/// Each camera that used such a merger will be paused.
		/// </summary>
		/// <param name="sender">The id of the application this merger belongs to.</param>
		/// <param name="mergerName">The name of the merger to remove. 
		///		Should be <c>null</c> to remove all mergers of this id.</param>
		private static void RemoveRemoteMerger(byte sender, String mergerName) {
			// search for cams that use disconnected remote mergers
			// set their merger to the local default merger and pause them
			bool foundAny = false;
			foreach (SensorCam cam in Program.Cameras) {
				if (!cam.Merger.IsLocal() && ((RemoteMerger)cam.Merger).NetworkID == sender
					&& (mergerName == null || cam.Merger.Name == mergerName)) {
					foundAny = true;
					cam.Merger = Program.Merger;
					cam.Pause();
				}
			}
			if (foundAny)
				Program.Warn("A remote merger in use has been removed, affected cams have been paused");

			// remove remote mergers of the disconnected ID from the list
			Program.RemoteMergers.RemoveAll(new Predicate<RemoteMerger>(target => {
				return target.NetworkID == sender
					&& (mergerName == null || target.Name == mergerName);
			}));
		}
		

	}
}
